﻿using System;
using System.Collections.Generic;
using System.Collections.Concurrent;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;

namespace Messaging2
{
    public sealed class Transport : IDisposable, ITransport
    {
        public IListener Listener { get; set; }
        private readonly Connection _connListen, _connSend;
        private readonly ConcurrentDictionary<string, IMessage> _cacheMessages;
        private readonly ConcurrentBag<string> _cacheTopics;
        private Byte[] _buffer = null;

        //construct
        public Transport(string address, int port, IPAddress endpoint = null)
        {
            _cacheMessages = new ConcurrentDictionary<string, IMessage>();
            _cacheTopics = new ConcurrentBag<string>();
            _connSend = new Connection(address, port);
            _connListen = new Connection(address, port, IPAddress.Any);
            Listener = new Listener(_connListen, _cacheTopics);
            Listener.MessageResend += Message_Resend;
        }

        //public
        public string Publish(IMessage message)
        {
            try
            {
                _buffer = Encoding.Unicode.GetBytes(message.ToString());
                _connSend.Client.Send(_buffer, _buffer.Length, _connSend.Endpoint);
                
                if (!message.RequestResendCache)//dont want to locally cache resend request
                {
                    _cacheMessages[message.Topic] = message;
                    string topic;
                    if (!_cacheTopics.TryPeek(out topic)) 
                        _cacheTopics.Add(message.Topic);
                }

                return "Published";
            }
            catch (Exception e)
            {
                return e.ToString();
            }
        }

        public void Listen(string topic, bool requestCache = false)
        {
            Listener.Listen(topic); //single listener per transport;
            if (requestCache) Publish(new Message(topic +"|String|true|"));
        }
        
        public void Dispose()
        {
            if(Listener!=null) Listener.Dispose();
            if(_connSend !=null) _connSend.Dispose();
            if(_connListen != null) _connListen.Dispose();
        }

        //events
        private void Message_Resend(object sender, EventArgs a)
        {
            //if has send back out at moment all listeners will get
            var res = (MessageEventArgs)a;
            IMessage val;
            if (_cacheMessages.TryGetValue(res.Message.Topic, out val))
                Publish(val);
        }
    }

    internal class Connection: IDisposable
    {
        public UdpClient Client = new UdpClient();
        public IPEndPoint Endpoint;
        public IPAddress Multicastaddress;

        //construct
        public Connection(string address, int port)
        {
            Multicastaddress = IPAddress.Parse(address);
            Client.ExclusiveAddressUse = false;
            Endpoint = new IPEndPoint(Multicastaddress, port);
            Client.JoinMulticastGroup(Multicastaddress);
        }

        public Connection(string address, int port, IPAddress endPoint)
        {
            Multicastaddress = IPAddress.Parse(address);
            Client.ExclusiveAddressUse = false;
            Endpoint = new IPEndPoint(endPoint == null ? IPAddress.Any : endPoint, port);
            Client.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
            Client.ExclusiveAddressUse = false;
            Client.Client.Bind(Endpoint);
            Client.JoinMulticastGroup(Multicastaddress);
        }

        //public
        public void Dispose()
        {
            if(Client != null) Client.Close();
        }
    }

    public class MessageEventArgs : EventArgs
    {
        public IMessage Message { get; set; }
        public MessageEventArgs(IMessage Message) { this.Message = Message; }
    }

    public enum ValType { String, Number, Array1D, Array2D, List, KVP};
    public enum ActType { New, Amend, Close, Unknown };
}
